﻿// Project: Salient
// http://salient.codeplex.com
// 
// Copyright 2010, Sky Sanders <sky at skysanders.net>
// Dual licensed under the MIT or GPL Version 2 licenses.
// http://salient.codeplex.com/license
// 
// Date: April 25 2010 

#region

using System;
using System.Collections;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

#endregion

namespace Salient.IO
{
    ///<summary>
    ///</summary>
    public static class ResourceLocator
    {
        /// <summary>
        /// If true, all resolved resources are cached internally.
        /// If set false, cache is cleared.
        /// </summary>
        public static bool CacheEnabled
        {
            get { return _cacheEnabled; }
            set
            {
                if (!value)
                {
                    _cache.Clear();
                }
                _cacheEnabled = value;
            }
        }

        #region Private Implementation

        private const string STR_CouldNotLoadReferencedAssembly = "Could not load referenced assembly:{0}";
        private const string STR_InvalidResourceUri = "Invalid resource uri:{0}";
        private const string STR_ReferencedAssemblyResourceNotFound = "Referenced assembly resource not found:{0}";

        private const string UriRxPattern = @"^(?<scheme>(?<nscheme>[^:/\?#]+):)?(?<auth>"
                                            + @"//(?<nauth>[^/\?#]*))?(?<path>[^\?#]*)"
                                            + @"(?<query>\?(?<nquery>[^#]*))?"
                                            + @"(?<frag>#(?<nfrag>.*))?";

        /// <summary>
        /// Cache resolved resources for quick retrieval
        /// </summary>
        private static readonly Hashtable _cache = new Hashtable();

        private static readonly Regex UriRx = new Regex(UriRxPattern,
                                                        RegexOptions.ExplicitCapture | RegexOptions.Compiled);

        private static bool _cacheEnabled;


        /// <summary>
        /// 
        /// </summary>
        /// <param name="uri"></param>
        /// <returns></returns>
        private static byte[] GetAssemblyResource(string uri)
        {
            string assemblyName, resourcePath;

            bool isAssemblyResourceUri = TryParseAssemblyResourceUri(uri, out assemblyName, out resourcePath);

            if (!isAssemblyResourceUri || string.IsNullOrEmpty(assemblyName) || string.IsNullOrEmpty(resourcePath))
            {
                throw new FileNotFoundException(String.Format(STR_InvalidResourceUri, uri));
            }

            byte[] returnValue = null;

            if (assemblyName == "*")
            {
                Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
                foreach (Assembly assembly in assemblies)
                {
                    returnValue = GetResouceBytes(assembly, resourcePath);
                    if (returnValue != null)
                    {
                        break;
                    }
                }
            }
            else
            {
                try
                {
                    Assembly a = Assembly.Load(assemblyName);
                    returnValue = GetResouceBytes(a, resourcePath);
                }
                catch (Exception ex)
                {
                    throw new FileNotFoundException(String.Format(STR_CouldNotLoadReferencedAssembly, assemblyName), ex);
                }
            }

            if (returnValue == null)
            {
                throw new FileNotFoundException(String.Format(STR_ReferencedAssemblyResourceNotFound, uri));
            }

            return returnValue;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="assembly"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        private static byte[] GetResouceBytes(Assembly assembly, string path)
        {
            byte[] returnValue = null;
            using (Stream s = assembly.GetManifestResourceStream(path))
            {
                if (s != null)
                {
                    returnValue = new byte[s.Length];
                    s.Read(returnValue, 0, (int) s.Length);
                }
            }

            return returnValue;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="uri"></param>
        /// <returns></returns>
        private static byte[] GetFileResource(string uri)
        {
            if (!File.Exists(uri))
            {
                throw new FileNotFoundException(uri);
            }
            byte[] returnValue;
            using (FileStream s = File.Open(uri, FileMode.Open))
            {
                returnValue = new byte[s.Length];
                s.Read(returnValue, 0, (int) s.Length);
            }
            return returnValue;
        }

        /// <summary>
        /// Parses a resource Uri (res://[*|assembly]/resource path)
        /// Necessary because Uri class chokes on the wildcard.
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="assemblyName"></param>
        /// <param name="resourcePath"></param>
        /// <returns></returns>
        private static bool TryParseAssemblyResourceUri(string uri, out string assemblyName, out string resourcePath)
        {
            assemblyName = null;
            resourcePath = null;

            Match m = UriRx.Match(uri);

            if (m.Success)
            {
                assemblyName = m.Groups["nauth"].Value;
                resourcePath = m.Groups["path"].Value.TrimStart(new[] {'/'});
            }

            return m.Success &&
                   string.Compare(m.Groups["nscheme"].Value, "res", true) == 0 &&
                   !string.IsNullOrEmpty(assemblyName) &&
                   !string.IsNullOrEmpty(resourcePath);
        }

        #endregion

        /// <summary>
        /// Resolves file path or resource uri and returns byte array.
        /// </summary>
        /// <param name="uri">valid file path or resource Uri res://[*|assemblyname]/resourcename</param>
        /// <returns></returns>
        public static byte[] GetResource(string uri)
        {
            if (_cache.ContainsKey(uri))
            {
                return (byte[]) _cache[uri];
            }

            byte[] returnValue = uri.StartsWith("res://", StringComparison.OrdinalIgnoreCase)
                                     ?
                                         GetAssemblyResource(uri)
                                     :
                                         GetFileResource(uri);

            if (_cacheEnabled)
            {
                _cache[uri] = returnValue;
            }

            return returnValue;
        }

        /// <summary>
        /// Resolves file path or resource uri and returns text using default encoding.
        /// </summary>
        /// <param name="uri">valid file path or resource Uri res://[*|assemblyname]/resourcename</param>
        /// <returns></returns>
        public static string GetTextResource(string uri)
        {
            return GetTextResource(uri, Encoding.Default);
        }

        /// <summary>
        /// Resolves file path or resource uri and returns text with specified encoding.
        /// </summary>
        /// <param name="uri">valid file path or resource Uri res://[*|assemblyname]/resourcename</param>
        /// <param name="encoding"></param>
        /// <returns></returns>
        /// <exception cref="FileNotFoundException"></exception>
        public static string GetTextResource(string uri, Encoding encoding)
        {
            byte[] bytes = GetResource(uri);
            string returnValue;
            using (MemoryStream mem = new MemoryStream(bytes))
            {
                using (StreamReader s = new StreamReader(mem, encoding))
                {
                    returnValue = s.ReadToEnd();
                }
            }
            return returnValue;
        }
    }
}